/**
 * Source monitor page, using the ATNF Javascript library.
 * Jamie Stevens 2012
 *
 * This script allows one to find the location of a source, and get
 * some details about where it is for the ATNF observatories.
 *
 */
require([ "dojo/_base/lang", "dojo/dom", "dojo/dom-construct",
	  "atnf/base", "atnf/time", "atnf/source", "atnf/sourceResolver",
	  "dojo/dom-attr", "dojo/number", "atnf/useful", "dojox/timing",
	  "atnf/atcaSchedule", "dojo/_base/Deferred", "atnf/angle",
	  "atnf/coordinate",
	  "dojo/domReady!" ],
  function(lang, dom, domConstruct, atnfLibrary, atnfTime, atnfSource,
    atnfSourceResolver, domAttr, number, atnfUseful, dTime,
    atcaSchedule, Deferred, atnfAngle, atnfCoord) {

    // Make a source resolver object.
    var sourceResolver = atnfSourceResolver.new({
      resolverScript: '/cgi-bin/sourcequery.pl'
    });

    var ourSources = [];

    // Set the default telescope to Parkes.
    //atnfLibrary.setDefaultLocation('ASKAP');

    var globalTime = atnfTime.new();
    // Get the Sun's coordinate.
    var solarCoord = atnfCoord.solarRADec(globalTime);

    var statusMessage = null;

    var plotElevations = [ 20, 30, 40, 50, 60, 70, 80 ];

    var findSource = function(sourceName) {
      // Check all the sources we have for a particular source name.
      for (var i = 0; i < ourSources.length; i++) {
	if (ourSources[i].details().name === sourceName) {
	  return ourSources[i];
	}
      }

      return null;
    };

    var tableSafeId = function(source, cellType) {
      var tableId = cellType + '_' + source;
      return tableId.replace(/[\-\+\s]+/g, '_');
    };

    var addSource = function(aSource) {
      // Stop the status message display.
      statusMessage.stop();

      if (!atnfLibrary.isSource(aSource.source)) {
	// This isn't a valid source, so we exit.
	var sourceEntry = domAttr.get(evtObj.target.id, 'value');
	domAttr.set('sourceNameStatus', 'innerHTML',
	  'Unable to resolve ' + sourceEntry + '.');
	return;
      }
      var sd = aSource.source.details();

      // Check if we already know about these sources.
      if (findSource(sd.name) !== null) {
	// We don't need to add this.
	return;
      }

      // Get the source details.
      domAttr.set('sourceNameStatus', 'innerHTML',
	'Successfully resolved ' + sd.name + '.');
      // Empty the entry box.
      domAttr.set('sourceNameInput', 'value', '');

      // Make a new table row, and attach it to the table.
      var nRow = domConstruct.create('tr');
      dom.byId('sourceTable').appendChild(nRow);

      var nameCell = domConstruct.create('td', {
	innerHTML: sd.name
      });
      nRow.appendChild(nameCell);

      // Get the coordinates in J2000.
      var jCo = sd.coordinate.toJ2000();

      var raCell = domConstruct.create('td', {
	innerHTML: atnfLibrary.turns2hexa(jCo.rightAscension.toTurns(), {
	  units: 'hours'
	})
      });
      nRow.appendChild(raCell);
      var decCell = domConstruct.create('td', {
	innerHTML: atnfLibrary.turns2hexa(jCo.declination.toTurns(), {
	  alwaysSigned: true
	})
      });
      nRow.appendChild(decCell);

      // Put some extra information in the table.
      var haCell = domConstruct.create('td', {
	id: tableSafeId(sd.name, 'hourAngle')
      });
      nRow.appendChild(haCell);
      var azCell = domConstruct.create('td', {
	id: tableSafeId(sd.name, 'azimuth')
      });
      nRow.appendChild(azCell);
      var elCell = domConstruct.create('td', {
	id: tableSafeId(sd.name, 'elevation')
      });
      nRow.appendChild(elCell);
      var risenCell = domConstruct.create('td', {
	id: tableSafeId(sd.name, 'risen')
      });
      nRow.appendChild(risenCell);
      var timeSetRiseCell = domConstruct.create('td', {
	id: tableSafeId(sd.name, 'timeSetRise')
      });
      nRow.appendChild(timeSetRiseCell);

      // Tell it to use our own global time object.
      aSource.source.useTime(globalTime);

      ourSources.push(aSource.source);
      drawObservability(true);
    };

    var addScheduleSource = function(aSource) {
      if (!atnfLibrary.isSource(aSource)) {
	// This isn't a valid source, so we exit.
	var sourceEntry = domAttr.get(evtObj.target.id, 'value');
	domAttr.set('sourceNameStatus', 'innerHTML',
	  'First scan source is invalid.');
	return;
      }
      var sd = aSource.details();

      // Check if we already know about these sources.
      if (findSource(sd.name) !== null) {
	// We don't need to add this.
	return;
      }

      // Get the source details.
      domAttr.set('sourceNameStatus', 'innerHTML',
	'Successfully resolved ' + sd.name + '.');
      // Empty the entry box.
      domAttr.set('sourceNameInput', 'value', '');

      // Make a new table row, and attach it to the table.
      var nRow = domConstruct.create('tr');
      dom.byId('sourceTable').appendChild(nRow);

      var nameCell = domConstruct.create('td', {
	innerHTML: sd.name
      });
      nRow.appendChild(nameCell);

      // Get the coordinates in J2000.
      var jCo = sd.coordinate.toJ2000();

      var raCell = domConstruct.create('td', {
	innerHTML: atnfLibrary.turns2hexa(jCo.rightAscension.toTurns(), {
	  units: 'hours'
	})
      });
      nRow.appendChild(raCell);
      var decCell = domConstruct.create('td', {
	innerHTML: atnfLibrary.turns2hexa(jCo.declination.toTurns(), {
	  alwaysSigned: true
	})
      });
      nRow.appendChild(decCell);

      // Put some extra information in the table.
      var haCell = domConstruct.create('td', {
	id: tableSafeId(sd.name, 'hourAngle')
      });
      nRow.appendChild(haCell);
      var azCell = domConstruct.create('td', {
	id: tableSafeId(sd.name, 'azimuth')
      });
      nRow.appendChild(azCell);
      var elCell = domConstruct.create('td', {
	id: tableSafeId(sd.name, 'elevation')
      });
      nRow.appendChild(elCell);
      var risenCell = domConstruct.create('td', {
	id: tableSafeId(sd.name, 'risen')
      });
      nRow.appendChild(risenCell);
      var timeSetRiseCell = domConstruct.create('td', {
	id: tableSafeId(sd.name, 'timeSetRise')
      });
      nRow.appendChild(timeSetRiseCell);

      // Tell it to use our own global time object.
      aSource.useTime(globalTime);

      ourSources.push(aSource);
      drawObservability(true);
    };

    var tableUpdates = function() {
      // This routine gets called periodically to update the table.

      // Update our global time to be now.
      globalTime.now();

      for (var i = 0; i < ourSources.length; i++) {
	var osDet = ourSources[i].details();

	// Update the hour angle.
	var haId = tableSafeId(osDet.name, 'hourAngle');
	var haString = atnfLibrary.turns2hexa(ourSources[i].hourAngle().toTurns(), {
	  units: 'hours',
	  precision: 0,
	  alwaysSigned: true
	});
	domAttr.set(haId, 'innerHTML', haString);
	// Update the azimuth and elevation.
	var currAzEl = ourSources[i].getAzEl();
	var azId = tableSafeId(osDet.name, 'azimuth');
	var azString = atnfLibrary.zeroTrail(
	  atnfLibrary.leadingPlus(atnfLibrary.zeroPad(
	    number.round(currAzEl.azimuth.toDegrees(), 3), 100)), 8);
	domAttr.set(azId, 'innerHTML', azString);
	var elId = tableSafeId(osDet.name, 'elevation');
	var elString = atnfLibrary.zeroTrail(
	  atnfLibrary.leadingPlus(atnfLibrary.zeroPad(
	    number.round(currAzEl.elevation.toDegrees(), 3), 10)), 7);
	domAttr.set(elId, 'innerHTML', elString);

	// Update the rise/set state and time.
	var risenId = tableSafeId(osDet.name, 'risen');

	// Get our location.
	var oLocation = atnfLibrary.getLocation(globalTime.getLocation());
	var horizonElevation = oLocation.limits.elevationLow;

	var riseAngle = currAzEl.elevation.subtract(horizonElevation).
	  toDegrees();
	if (riseAngle < 0) {
	  domAttr.set(risenId, 'innerHTML', 'R');
	} else {
	  domAttr.set(risenId, 'innerHTML', 'S');
	}
	var setId = tableSafeId(osDet.name, 'timeSetRise');
	var setHa = ourSources[i].timeUntilElevation(horizonElevation);
	if (setHa.toTurns() > 1) {
	  domAttr.set(setId, 'innerHTML', 'never rises');
	} else if (setHa.toTurns() < -1) {
	  domAttr.set(setId, 'innerHTML', 'never sets');
	} else {
	  var haTime = atnfLibrary.turns2hexa(setHa.toTurns(), {
	    units: 'hours',
	    precision: 0
	  });
	  domAttr.set(setId, 'innerHTML', haTime);
	}
      }
    };

    var lookupSource = function(evtObj) {
      // Get the entry.
      var sourceEntry = domAttr.get(evtObj.target.id, 'value');

      // Put a status entry onto the page to let the user know what's happening.
      statusMessage = atnfUseful.generateStatusMessage(
	'Finding position for ' + sourceEntry, 'sourceNameStatus');
      statusMessage.start();

      // Lookup the source.
      sourceResolver.setSourceName(sourceEntry).resolveName().
	then(addSource);
    };

    /**
     * Draw the observability plot on the canvas.
     * @param {boolean} full If true, redraw the entire plot, otherwise
     *                       only add the last source on our list.
     */
    var drawObservability = function(full) {
      // Check that we actually support the Canvas.
      if (!!document.createElement('testcanvas').getContext) {
	console.log("Can't make Canvas elements with this browser.");
	return;
      }

      // Get our Canvas.
      var obsCanvas = dom.byId('observabilityPlot');

      // Some plot size parameters.
      var params = {
	line: {
	  width: 2,
	  cap: 'square'
	},
	borderSize: 20,
	source: {
	  font: 'News Cycle',
	  fontSize: 16,
	  textAlign: 'center',
	  textBaseline: 'bottom',
	  fillStyle: '#000000',
	  baseline: 3,
	  clearance: 3
	},
	elevation: {
	  font: 'News Cycle',
	  fontSize: 14,
	  textAlign: 'center',
	  textBaseline: 'top',
	  fillStyle: '#000000',
	  baseline: 1,
	  clearance: 4,
	  tickLength: -3
	},
	clearance: {
	  left: 6,
	  right: 6,
	  top: 10,
	  bottom: 10
	},
	widthFraction: 0.9,
	window: {},
	canvas: {},
	axis: {
	  x: {
	    font: 'News Cycle',
	    fontSize: 20,
	    textAlign: 'center',
	    textBaseline: 'top',
	    fillStyle: '#000000',
	    tickLengths: {
	      major: 10,
	      minor: 5
	    }
	  }
	}
      };
      // And some derived parameters.
      params.source.total = params.source.fontSize + params.source.baseline +
	params.source.clearance;
      params.elevation.total = params.elevation.fontSize + params.elevation.baseline +
	params.elevation.clearance;
      params.totalHeight = params.source.total + params.elevation.total +
	params.line.width;

      // Work out how big to make the plot.
      params.window.width = window.innerWidth;
      params.window.requiredWidth = params.widthFraction *
	params.window.width;
      params.window.requiredHeight = params.clearance.top + params.clearance.bottom +
	ourSources.length * params.totalHeight +
	4 * params.borderSize;

      // Set the width and height of the canvas.
      obsCanvas.width = params.window.requiredWidth;
      obsCanvas.height = params.window.requiredHeight;
      params.window.height = obsCanvas.height;
      params.window.width = obsCanvas.width;

      params.boxArea = {
	x: [ params.borderSize, params.window.width - params.borderSize ],
	y: [ 2 * params.borderSize,
	    params.window.height - 2 * params.borderSize ]
      };
      params.boxArea.xLength = params.boxArea.x[1] - params.boxArea.x[0];
      params.boxArea.yLength = params.boxArea.y[1] - params.boxArea.y[0];

      // Get our context.
      var context = obsCanvas.getContext('2d');
      context.lineWidth = params.line.width;

      // Make the canvas have a white background.
      context.fillStyle = '#ffffff';
      context.fillRect(0, 0, params.window.width, params.window.height);

      // Make a box that will contain the plot.
      context.strokeStyle = '#000000';
      context.strokeRect(params.boxArea.x[0], params.boxArea.y[0],
	params.boxArea.xLength, params.boxArea.yLength);

      // Make the LST axis across the bottom.
      params.axis.x.length = params.boxArea.xLength;
      var sHourLength = params.axis.x.length / 24;

      var fontString = function(o) {
	return "" + o.fontSize + "px '" + o.font + "'";
      };

      var setTextProperties = function(o) {
	context.font = fontString(o);
	context.textBaseline = o.textBaseline;
	context.textAlign = o.textAlign;
	context.fillStyle = o.fillStyle;
      };

      // Set up the tick line properties.
      context.lineCap = params.line.cap;
      // Set up the text properties.
      setTextProperties(params.axis.x);

      var drawTick = function(x, y, length, dir) {
	context.beginPath();
	context.moveTo(x, y);
	context.lineTo(
	  (dir === 0) ? x + length : x,
	  (dir === 1) ? y + length : y
	);
	context.stroke();
	context.closePath();
      };

      // Label the axis, and make the ticks.
      for (var lh = 0; lh <= 24; lh++) {
	var xPos = params.boxArea.x[0] + lh * sHourLength;
	var yPos = params.boxArea.y[1];
	if (lh % 3 === 0) {
	  if (lh > 0 && lh < 24) {
	    drawTick(xPos, yPos,
	      -1 * params.axis.x.tickLengths.major, 1);
	  }
	  context.fillText('' + lh, xPos, yPos);
	} else {
	  drawTick(xPos, yPos,
	    -1 * params.axis.x.tickLengths.minor, 1);
	}
      }

      // Make the UTC axis across the top.
      var d = new Date();
      context.textBaseline = 'bottom';
      for (var ut = 0; ut < 24; ut++) {
	d.setUTCHours(ut, 0, 0, 0);
	var t = atnfTime.new({ 'date': d });
	var lt = t.lmst() * 24;
	var xPos = params.boxArea.x[0] + lt * sHourLength;
	var yPos = params.boxArea.y[0];
	if (ut % 3 === 0) {
	  drawTick(xPos, yPos,
	    params.axis.x.tickLengths.major, 1);
	  context.fillText('' + ut, xPos, yPos);
	} else {
	  drawTick(xPos, yPos,
	    params.axis.x.tickLengths.minor, 1);
	}
      }

      // Go through our sources.
      var plotTopY = params.boxArea.y[0] +
	params.axis.x.tickLengths.major; // Includes tick length.
      for (var i = 0; i < ourSources.length; i++) {
	setTextProperties(params.elevation);
	var sd = ourSources[i].details();
	// Get the rise LST as an absolute hour.
	var lr = ourSources[i].lstRise().toHours();
	// And the set LST.
	var ls = ourSources[i].lstSet().toHours();
	// Draw a line from this position to the set time or
	// right edge.
	var yPos = plotTopY + i * params.totalHeight +
	  params.source.total;
	var yBottomPos = yPos + params.elevation.baseline;
	var noRise = false;
	var xPosR, xPosS;
	if (lr < 0) {
	  // This source never sets.
	  xPosR = params.boxArea.x[0];
	  xPosS = params.boxArea.x[1];
	  drawTick(params.boxArea.x[0], yPos, params.boxArea.xLength, 0);
	} else if (lr >= 24) {
	  // This source never rises.
	  noRise = true;
	} else {
	  xPosR = params.boxArea.x[0] + lr * sHourLength;
	  var xLength = (lr < ls) ? (ls - lr) * sHourLength :
	    params.boxArea.x[1] - xPosR;
	  drawTick(xPosR, yPos, xLength, 0);
	  drawTick(xPosR, yPos, params.elevation.tickLength, 1);
	  // Get the elevation limit for this source.
	  var lim = atnfLibrary.getLocation(
	    sd.coordinate.getLocation()).limits.
	    elevationLow.toDegrees();
	  context.fillText('' + lim, xPosR, yBottomPos);
	  // Draw a line from the left edge to this position
	  // if required.
	  xPosS = params.boxArea.x[0] + ls * sHourLength;
	  xLength = xPosS - params.boxArea.x[0];
	  if (lr > ls) {
	    drawTick(xPosS, yPos, -1 * xLength, 0);
	  }
	  drawTick(xPosS, yPos, params.elevation.tickLength, 1);
	  context.fillText('' + lim, xPosS, yBottomPos);
	}
	if (!noRise) {
	  // Draw the RA marker.
	  var tRa = sd.coordinate.toJ2000().rightAscension.toHours();
	  var xPosL = params.boxArea.x[0] + tRa * sHourLength;
	  drawTick(xPosL, yPos, -2 * params.elevation.tickLength, 1);
	  // Draw all the elevation markers.
	  for (var e = 0; e < plotElevations.length; e++) {
	    if (plotElevations[e] <= lim) {
	      continue;
	    }
	    // Get the LST at this elevation.
	    var eAngle = atnfAngle.new(plotElevations[e]);
	    var rl = ourSources[i].lstRise(eAngle).toHours();
	    var xPosE;
	    if (rl >= 0 && rl < 24) {
	      xPosE = params.boxArea.x[0] + rl * sHourLength;
	      drawTick(xPosE, yPos, params.elevation.tickLength, 1);
	      context.fillText('' + plotElevations[e], xPosE, yBottomPos);
	    }
	    var rs = ourSources[i].lstSet(eAngle).toHours();
	    if (rs >= 0 && rs < 24) {
	      xPosE = params.boxArea.x[0] + rs * sHourLength;
	      drawTick(xPosE, yPos, params.elevation.tickLength, 1);
	      context.fillText('' + plotElevations[e], xPosE, yBottomPos);
	    }
	  }
	}
	// Make the source name appear.
	setTextProperties(params.source);
	var textm = context.measureText(sd.name);
	var yTopPos = yPos - params.source.baseline;
	if (noRise) {
	  var cPos = params.boxArea.xLength / 2 +
	    params.boxArea.x[0];
	  context.fillText(sd.name + ' (never rises)', cPos, yTopPos);
	} else if (lr < ls || lr >= 24) {
	  var cPos = (xPosS - xPosR) / 2 + xPosR;
	  context.fillText(sd.name, cPos, yTopPos);
	} else {
	  var rLen = params.boxArea.x[1] - xPosR;
	  if (rLen > textm.width) {
	    var cPos = xPosR + rLen / 2;
	    context.fillText(sd.name, cPos, yTopPos);
	  } else {
	    // Make the text finish at the side.
	    context.textAlign = 'right';
	    var rPos = params.boxArea.x[1] - params.clearance.right;
	    context.fillText(sd.name, rPos, yTopPos);
	    context.textAlign = 'center';
	  }
	  var lLen = xPosS - params.boxArea.x[0];
	  if (lLen > textm.width) {
	    var cPos = xPosS - lLen / 2;
	    context.fillText(sd.name, cPos, yTopPos);
	  } else {
	    // Make the text start at the side.
	    context.textAlign = 'left';
	    var lPos = params.boxArea.x[0] + params.clearance.left;
	    context.fillText(sd.name, lPos, yTopPos);
	    context.textAlign = 'center';
	  }
	}
      }
    };

    // Connect the source name input box.
    atnfUseful.onEnter('sourceNameInput', lookupSource);

    // Load a schedule and put the first source in the table.
    var scheduleHandler = atcaSchedule.scheduleHandler();
    Deferred.when(scheduleHandler.load('ste616_kteri.sch'),
      function(sch) {
	// We check for a defined handler upon return.
	if (sch) {
	  // Get the first scan.
	  var scan = sch.getScan(1);
	  addScheduleSource(scan.source);
	}
      });

    // Start updating the table every second.
    var tableTimer = new dTime.Timer();
    tableTimer.setInterval(1000);
    tableTimer.onTick = tableUpdates;
    tableTimer.start();
  });
